package com.siashan.toolkit.crypt.digest;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;

/**
 * GNU libc crypt（3）兼容哈希方法.
 *
 * 这个类是不可变的，并且是线程安全的.
 * @author siashan
 * @since 1.0.7
 */
public class Crypt {

    /**
     * 以与密码（3）兼容的方式加密密码.
     * <p>
     * 使用随机salt和默认算法（当前为SHA-512）.
     * </p>
     * <p>
     *
     * @param keyBytes
     *            明文密码
     * @return 哈希值
     */
    public static String crypt(final byte[] keyBytes) {
        return crypt(keyBytes, null);
    }

    /**
     * 以与密码（3）兼容的方式加密密码.
     * <p>
     * 如果未提供salt，则将使用随机salt和默认算法（当前为SHA-512）.
     *
     * @param keyBytes
     *            明文密码
     * @param salt
     *            不带前缀或“rounds=”的实际盐值。
     *            盐可能是空的，在这种情况下，将使用{@link-ThreadLocalRandom}为您生成一个salt；
     *            对于更安全的盐，请考虑使用{Link SaleRealDANDOM }生成你自己的盐。
     * @return 哈希值
     */
    public static String crypt(final byte[] keyBytes, final String salt) {
        if (salt == null) {
            return Sha2Crypt.sha512Crypt(keyBytes);
        } else if (salt.startsWith(Sha2Crypt.SHA512_PREFIX)) {
            return Sha2Crypt.sha512Crypt(keyBytes, salt);
        } else if (salt.startsWith(Sha2Crypt.SHA256_PREFIX)) {
            return Sha2Crypt.sha256Crypt(keyBytes, salt);
        } else if (salt.startsWith(Md5Crypt.MD5_PREFIX)) {
            return Md5Crypt.md5Crypt(keyBytes, salt);
        } else {
            return UnixCrypt.crypt(keyBytes, salt);
        }
    }

    /**
     * 使用最强加密（3）算法计算摘要.
     * <p>
     * 使用随机salt和默认算法（当前为SHA-512）。
     * </p>
     * <p>
     * 使用{@link-ThreadLocalRandom}为您生成一个salt；
     * 为了更安全的盐考虑使用{@link SecureRandom}生成您自己的salt并调用{@link#crypt（String，String）}.
     * </p>
     *
     * @param key
     *            明文密码
     * @return 哈希值
     */
    public static String crypt(final String key) {
        return crypt(key, null);
    }

    /**
     * 以与密码（3）兼容的方式加密密码.
     * <p>
     * 精确的算法取决于salt字符串的格式:
     * <ul>
     * <li>SHA-512盐以{@code$6$}开头，最长可达16个字符.
     * <li>SHA-256盐以{@code$5$}开头，最长可达16个字符
     * <li>MD5盐以{@code$1$}开头，最长可达8个字符
     * <li>DES，传统的UnixCrypt算法仅使用2个字符
     * <li>DES算法中只使用密码的前8个字符！
     * </ul>
     * 此方法无法识别神奇字符串{@code“$apr1$”}和{@code“$2a$”}，因为其输出应该是与libc实现相同。
     * <p>
     * 盐串的其余部分从集合{@code[a-zA-Z0-9./]}中提取，
     * 并在a的最大长度处切割遇到{@code“$”}符号。
     * 因此，输入完整的哈希值作为salt是有效的，例如验证密码为：
     *
     * <pre>
     * storedPwd.equals(crypt(enteredPwd, storedPwd))
     * </pre>
     * <p>
     * 结果字符串以标记字符串（{@code$n$}）开头，其中n与输入字符串相同.
     * 然后附加salt，后跟{@code“$”}符号.
     * 后面是实际的散列值.
     * 对于DES，字符串只包含salt和实际哈希.
     * 总长度取决于使用的算法：
     * <ul>
     * <li>SHA-512:106个字符
     * <li>SHA-256: 63 个字符
     * <li>MD5: 34 个字符
     * <li>DES: 13 个字符
     * </ul>
     * <p>
     * Example:
     *
     * <pre>
     *      crypt("secret", "$1$xxxx") =&gt; "$1$xxxx$aMkevjfEIpa35Bh3G4bAc."
     *      crypt("secret", "xx") =&gt; "xxWAum7tHdIUw"
     * </pre>
     * <p>
     * 此方法的变体接受byte[]数组以支持未编码的输入字符串UTF-8，
     * 但例如在ISO-8859-1中，相同的字符导致不同的字节值.
     *
     * @param key
     *            由所用用户输入的明文密码
     * @param salt
     *            不带前缀或“rounds=”的实际盐值。
     *            盐可能为空，在这种情况下salt是使用{@link-ThreadLocalRandom}为您生成的；
     *            为了更安全的盐考虑使用{@Link SoalEnrand On}来生成自己的盐。
     * @return 散列值，即包括salt字符串的加密密码
     */
    public static String crypt(final String key, final String salt) {
        return crypt(key.getBytes(StandardCharsets.UTF_8), salt);
    }
}
